package jmine.tec.sync;

import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jmine.tec.persist.PersistMessages;
import jmine.tec.rpc.common.entity.EntityRepresentation;
import jmine.tec.rpc.common.entity.EntityRest;
import jmine.tec.rpc.common.provider.RestServiceProvider;
import jmine.tec.sync.dao.RemoteEntityReferenceDAO;
import jmine.tec.sync.domain.RemoteEntityReference;
import jmine.tec.sync.domain.enumx.SynchronizationOperationType;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.dao.BeanNotFoundException;

/**
 * Implementacao abstrata de {@link EntityMaterializer}. Por default, assume que a entidade remota e a local sao iguais,
 * 
 * @author takeshi
 * @param <A> o tipo do dominio local
 */
public abstract class AbstractEntityMaterializer<A extends Persistable> implements EntityMaterializer<A> {

    private static final int POSTSCRIPT_THRESHOLD = 100000;

    private RestServiceProvider restServiceProvider;

    private RemoteEntityReferenceDAO entityReferenceDAO;

    private BaseDAOFactory baseDAOFactory;

    private final Log logger = LogFactory.getLog(this.getClass());

    private DatabaseSynchronizationListener databaseSynchronizationListener;

    /**
     * {@inheritDoc}
     */
    public void entityDeleted(Serializable remotePrimaryKey) {
        if (!this.isMappedByNaturalKey()) {
            try {
                RemoteEntityReference reference =
                        this.getEntityReferenceDAO().findByEntityNameRemoteIdSource(this.getLocalEntityName(), this.getRemoteEntityName(),
                                (Long) remotePrimaryKey, this.getSource());
                Persistable persistable = reference.recoverLocalCopy();
                persistable.getPersister().remove();
                reference.getPersister().remove();
            } catch (BeanNotFoundException e) {

                this.beanNotFounOnDelete(remotePrimaryKey, this.getLocalEntityName(),
                        PersistMessages.BEAN_NOT_FOUND_BY_NATURAL_KEY.create(this.getLocalBaseDAO().getClass()).getMessage(), this
                                .getDatabaseSynchronizationListener().getSource(), null, SynchronizationOperationType.DELETE, null);
            }
        } else if (this.isMappedDirectly(remotePrimaryKey)) {
            BaseDAO<A> localDAO = this.getLocalBaseDAO();
            try {
                localDAO.findByPk(remotePrimaryKey).getPersister().remove();
            } catch (BeanNotFoundException e) {

                this.beanNotFounOnDelete(remotePrimaryKey, this.getLocalEntityName(),
                        PersistMessages.BEAN_NOT_FOUND_BY_NATURAL_KEY.create(this.getLocalBaseDAO().getClass()).getMessage(), this
                                .getDatabaseSynchronizationListener().getSource(), null, SynchronizationOperationType.DELETE, null);
            }
        }
    }

    /**
     * Deve ser chamado em subclasses para definir o que deve ser feito caso nao seja possivel deletar uma entidade
     * 
     * @param remotePrimaryKey {@link Serializable}
     * @param entityClass classe da entidade na origem
     * @param message messagem de erro
     * @param entitySource origem
     * @param naturalKey natural key da entidade
     * @param typeOperation tipo da operação que estava sendo realizada
     * @param entity representação da entidade.
     */
    protected void beanNotFounOnDelete(Serializable remotePrimaryKey, String entityClass, String message, String entitySource,
            Serializable naturalKey, SynchronizationOperationType typeOperation, InputStream entity) {
        this.getDatabaseSynchronizationListener().createSynchronizationErrorEntity(remotePrimaryKey, entityClass, message, entitySource,
                naturalKey, typeOperation, null);

        if (naturalKey == null) {
            this.beanNotFounOnDelete(remotePrimaryKey);
        } else {
            if (this.getLogger().isInfoEnabled()) {
                this.getLogger().info(
                        "Bean of type: " + this.localEntityClass() + " not found for remote id: " + remotePrimaryKey + " and natural key: "
                                + naturalKey);
            }
        }
    }

    /**
     * @return {@link Log}
     */
    protected final Log getLogger() {
        return this.logger;
    }

    /**
     * Define o comportamento que deve ser tomado caso houver uma tentativa de deletar uma entidade pelo seu natural key, mas a entidade
     * local nao for encontrada
     * 
     * @param remotePrimaryKey {@link Serializable}
     */
    protected void beanNotFounOnDelete(Serializable remotePrimaryKey) {
        if (this.getLogger().isInfoEnabled()) {
            this.getLogger().info("Bean of type: " + this.localEntityClass() + " not found for remote id: " + remotePrimaryKey);

        }
    }

    /**
     * Devolve o {@link BaseDAO} referente a entidade local
     * 
     * @return {@link BaseDAO} de A
     */
    protected BaseDAO<A> getLocalBaseDAO() {
        BaseDAO<A> localDAO = this.getBaseDAOFactory().getGenericDAO(this.localEntityClass());
        return localDAO;
    }

    /**
     * Devolve o nome da entidade remota, que por defaule eh a mesma que a local
     * 
     * @return {@link String}
     */
    public String getRemoteEntityName() {
        return this.getLocalEntityName();
    }

    /**
     * {@inheritDoc}
     */
    public void entityDeleted(Serializable remotePrimaryKey, Serializable naturalKey) {
        String classe = this.getLocalBaseDAO().getClass().getSimpleName();
        if (this.isMappedByNaturalKey()) {
            try {
                this.getLocalBaseDAO().findByNaturalKey(naturalKey).getPersister().remove();
            } catch (BeanNotFoundException e) {

                this.beanNotFounOnDelete(remotePrimaryKey, classe,
                        PersistMessages.BEAN_NOT_FOUND_BY_NATURAL_KEY.create(this.getLocalBaseDAO().getClass()).getMessage(), this
                                .getDatabaseSynchronizationListener().getSource(), naturalKey, SynchronizationOperationType.DELETE, null);
            }
        } else if (this.isMappedDirectly(remotePrimaryKey)) {
            try {
                this.getLocalBaseDAO().findByPk(remotePrimaryKey).getPersister().remove();
            } catch (BeanNotFoundException e) {

                this.beanNotFounOnDelete(remotePrimaryKey, classe,
                        PersistMessages.BEAN_NOT_FOUND_BY_NATURAL_KEY.create(this.getLocalBaseDAO().getClass()).getMessage(), this
                                .getDatabaseSynchronizationListener().getSource(), naturalKey, SynchronizationOperationType.DELETE, null);
            }
        } else {
            this.entityDeleted(remotePrimaryKey);
        }
    }

    /**
     * @return the baseDAOFactory
     */
    public BaseDAOFactory getBaseDAOFactory() {
        return this.baseDAOFactory;
    }

    /**
     * @param baseDAOFactory the baseDAOFactory to set
     */
    public void setBaseDAOFactory(BaseDAOFactory baseDAOFactory) {
        this.baseDAOFactory = baseDAOFactory;
    }

    /**
     * @return the source
     */
    public String getSource() {
        return this.getDatabaseSynchronizationListener().getSource();
    }

    /**
     * @return the entityReferenceDAO
     */
    public RemoteEntityReferenceDAO getEntityReferenceDAO() {
        if (this.entityReferenceDAO == null) {
            this.entityReferenceDAO = this.getBaseDAOFactory().getDAOByClass(RemoteEntityReferenceDAO.class);
        }
        return this.entityReferenceDAO;
    }

    /**
     * @param entityReferenceDAO the entityReferenceDAO to set
     */
    public void setEntityReferenceDAO(RemoteEntityReferenceDAO entityReferenceDAO) {
        this.entityReferenceDAO = entityReferenceDAO;
    }

    /**
     * {@inheritDoc}
     */
    public A updateEntity(Serializable remotePrimaryKey, Serializable naturalKey) {
        try {
            return this.updateEntity(remotePrimaryKey, this.retrieveRepresentations(remotePrimaryKey));
        } catch (BeanNotFoundException e) {
            // entity was deleted. We may call deleted callback method
            this.entityDeleted(remotePrimaryKey, naturalKey);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public A updateEntity(Serializable remotePrimaryKey, Map<Class<?>, Object> representations) {
        try {
            Map<Object, Object> map = this.getDatabaseSynchronizationListener().getRepresentationCache();
            Tuple<Object, Object> tuple = UnmodifiableTuple.<Object, Object> newTuple(this.getClass(), remotePrimaryKey);

            A persistable = this.findPersistable(remotePrimaryKey, representations);
            if (persistable == null) {
                persistable = this.newEntity(representations, remotePrimaryKey, map, tuple);
            }
            map.put(tuple, persistable);
            this.doUpdateEntity(persistable, representations, remotePrimaryKey);
            this.persistEntity(persistable, remotePrimaryKey);
            return persistable;
        } catch (RuntimeException e) {
            this.logErrors(remotePrimaryKey, representations, e.getMessage(), e);
            throw e;
        }
    }

    /**
     * @param remotePrimaryKey the primary key
     * @param representations the representations
     * @param message the message
     * @param e the Exception
     */
    private void logErrors(Serializable remotePrimaryKey, Map<Class<?>, Object> representations, String message, Exception e) {
        for (Class<?> klass : representations.keySet()) {
            EntityRepresentation entityRepresentation = (EntityRepresentation) representations.get(klass);
            Serializable naturalKey =
                    this.isMappedByNaturalKey() ? this.naturalKeyFromRepresentations(remotePrimaryKey, representations) : null;
            this.getDatabaseSynchronizationListener().trackingError(entityRepresentation, remotePrimaryKey, naturalKey, message,
                    this.getRemoteEntityName(), SynchronizationOperationType.UPDATE, e);
        }
    }

    /**
     * Procura o persistable a ser atualizado / removido. Por padrao, procura primeiro sua {@link RemoteEntityReference}, depois verifica se
     * o mapeamento deve ser direto (bater o id) se {@link #isMappedDirectly(Serializable)}, procura por natural key se
     * {@link #isMappedByNaturalKey()} e se falhar em todos os casos, devolve <code>null</code>
     * 
     * @param remotePrimaryKey {@link Serializable}
     * @param representations {@link Map} com as representacoes
     * @return A ou null
     */
    protected A findPersistable(Serializable remotePrimaryKey, Map<Class<?>, Object> representations) {
        A byReference = this.findByRemoteReference(remotePrimaryKey);
        if (byReference != null) {
            return byReference;
        } else if (this.isMappedDirectly(remotePrimaryKey)) {
            try {
                return this.getLocalBaseDAO().findByPk(remotePrimaryKey);
            } catch (BeanNotFoundException e) {
                return null;
            }
        } else if (this.isMappedByNaturalKey()) {
            try {
                return this.getLocalBaseDAO().findByNaturalKey(this.naturalKeyFromRepresentations(remotePrimaryKey, representations));
            } catch (BeanNotFoundException e) {
                return null;
            }
        }
        return null;
    }

    /**
     * Procura a entidade pelo {@link RemoteEntityReference}
     * 
     * @param remotePrimaryKey {@link Serializable} com o id remoto
     * @return A ou null se nenhum encontrado
     */
    protected A findByRemoteReference(Serializable remotePrimaryKey) {
        try {
            RemoteEntityReference reference =
                    this.getEntityReferenceDAO().findByEntityNameRemoteIdSource(this.getLocalEntityName(), this.getRemoteEntityName(),
                            (Long) remotePrimaryKey, this.getSource());
            try {
                @SuppressWarnings("unchecked")
                A persistable = (A) reference.recoverLocalCopy();
                return persistable;
            } catch (BeanNotFoundException e) {
                reference.getPersister().remove();
            }
        } catch (BeanNotFoundException e) {
            return null;
        }
        return null;
    }

    /**
     * Cria a {@link RemoteEntityReference} para a entidade
     * 
     * @param remotePrimaryKey {@link Serializable} o id REMOTO
     * @param entity a copia local
     */
    protected void createRemoteEntityReference(Serializable remotePrimaryKey, A entity) {
        try {
            RemoteEntityReference reference =
                    this.getEntityReferenceDAO().findByEntityNameRemoteIdSource(this.getLocalEntityName(), this.getRemoteEntityName(),
                            (Long) remotePrimaryKey, this.getSource());
            if (!reference.getLocalEntityId().equals(entity.getPk())) {
                reference.setLocalEntityId((Long) entity.getPk());
                reference.getPersister().save();
            }
        } catch (BeanNotFoundException e) {
            RemoteEntityReference reference = this.getEntityReferenceDAO().createBean();
            reference.setLocalEntityId(((Number) entity.getPk()).longValue());
            reference.setLocalEntityName(this.getLocalEntityName());
            reference.setRemoteEntityId(((Number) remotePrimaryKey).longValue());
            reference.setRemoteEntityName(this.getRemoteEntityName());
            reference.setLocalEntityName(this.getLocalEntityName());
            reference.setSource(this.getSource());
            reference.getPersister().save();
        }
    }

    /**
     * {@inheritDoc}
     */
    public A getLocalCopyOrRetrieve(Serializable remoteId, Serializable naturalKey) throws BeanNotFoundException {
        Map<Object, Object> map = this.getDatabaseSynchronizationListener().getRepresentationCache();
        Tuple<Object, Object> tuple = UnmodifiableTuple.<Object, Object> newTuple(this.getClass(), remoteId);
        @SuppressWarnings("unchecked")
        A object = (A) map.get(tuple);
        if (object != null) {
            return object;
        }
        A ref = this.findByRemoteReference(remoteId);
        if (ref != null) {
            map.put(tuple, ref);
            return ref;
        }
        try {
            if (this.isMappedDirectly(remoteId)) {
                A representation = this.getLocalBaseDAO().findByPk(remoteId);
                map.put(tuple, representation);
                return representation;
            } else if (this.isMappedByNaturalKey()) {
                A representation = this.getLocalBaseDAO().findByNaturalKey(naturalKey);
                map.put(tuple, representation);
                return representation;
            }
        } catch (BeanNotFoundException e) {
            // we need to update things
        }
        return this.updateEntity(remoteId, this.retrieveRepresentations(remoteId));
    }

    /**
     * Busca as representacoes necessarias para materializar uma entidade
     * 
     * @param remoteId {@link Serializable}
     * @return {@link Map}
     * @throws BeanNotFoundException se a entidade nao for encontrada
     */
    protected Map<Class<?>, Object> retrieveRepresentations(Serializable remoteId) throws BeanNotFoundException {
        return retrieveRepresentationsFrom(remoteId, this.remoteRepresentations(), this.restServiceProvider);
    }

    /**
     * Devolve as representacoes dada a lista com os tipos de representacoes necessarias e o {@link RestServiceProvider}
     * 
     * @param remoteId {@link Serializable}
     * @param remoteRepresentations {@link List}
     * @param provider o provider
     * @return {@link Map}
     * @throws BeanNotFoundException se a entidade remota nao for encontrada
     */
    public static Map<Class<?>, Object> retrieveRepresentationsFrom(Serializable remoteId, List<? extends Class<?>> remoteRepresentations,
            RestServiceProvider provider) throws BeanNotFoundException {
        Map<Class<?>, Object> representationMap = new HashMap<Class<?>, Object>(remoteRepresentations.size());
        for (Class<?> representationType : remoteRepresentations) {
            @SuppressWarnings("unchecked")
            EntityRest<? extends EntityRepresentation> rest =
                    provider.provideByRepresentation((Class<? extends EntityRepresentation>) representationType);
            if (rest != null) {
                EntityRepresentation representation = rest.findByIdentification(remoteId);
                representationMap.put(representationType, representation);
            }
        }
        return representationMap;
    }

    /**
     * Metodo para se criar uma nova entidade. Por default, apenas pega o {@link #getLocalBaseDAO()} e chama {@link BaseDAO#createBean()}
     * 
     * @param representations {@link Map}
     * @param requestedId {@link Serializable}
     * @param localEntityCache {@link Map} o cache de objetos desta "sessao"
     * @param cacheKey a chave que deve ser usada para o cache
     * @return A
     */
    protected A newEntity(Map<Class<?>, Object> representations, Serializable requestedId, Map<Object, Object> localEntityCache,
            Object cacheKey) {
        A bean = this.getLocalBaseDAO().createBean();
        localEntityCache.put(cacheKey, bean);
        return bean;
    }

    /**
     * Devolve a classe local. Por default, tenta resolver o tipo concreto de A. Se as subclasses nao usarem generics, elas devem
     * sobrescrever este metodo
     * 
     * @return {@link Class}
     */
    @SuppressWarnings("unchecked")
    protected Class<A> localEntityClass() {
        return (Class<A>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(
                AbstractEntityMaterializer.class.getTypeParameters()[0], this.getClass());
    }

    /**
     * Devolve o nome da entidade local. Por default, chama {@link #localEntityClass()} e chama {@link Class#getName()}
     * 
     * @return String
     */
    public String getLocalEntityName() {
        return this.localEntityClass().getName();
    }

    /**
     * Verifica se uma entidade deve ser mapeada diretamente - ou seja, seu PK deve ser igual ao do sistema remoto. Por default, verifica se
     * a PK eh um {@link Number} e verifica se o seu valor eh menor do que {@value #POSTSCRIPT_THRESHOLD}
     * 
     * @param pk {@link Serializable}
     * @return boolean
     */
    protected boolean isMappedDirectly(Serializable pk) {
        return (pk instanceof Number) && (((Number) pk).longValue() < POSTSCRIPT_THRESHOLD);
    }

    /**
     * Deve devolver a natural key de uma entidade dadas as suas representacoes, se aplicavel - ou seja se esta entidade
     * {@link #isMappedByNaturalKey()}
     * 
     * @param remoteId {@link Serializable}
     * @param representations {@link Map}
     * @return {@link Serializable} ou <code>null</code> se nao aplicavel
     */
    public abstract Serializable naturalKeyFromRepresentations(Serializable remoteId, Map<Class<?>, Object> representations);

    /**
     * Metodo que deve ser sobrescrito por subclasses e deve efetivamente atualizar o objeto entity passado, dadas as informacoes contidas
     * no mapa de representacoes.
     * 
     * @param entity a entidade a ser atualizada
     * @param representations o {@link Map} com as representacoes
     * @param remoteId {@link Serializable}
     */
    protected abstract void doUpdateEntity(A entity, Map<Class<?>, Object> representations, Serializable remoteId);

    /**
     * @return {@link Session}
     */
    protected Session getCurrentHibernateSession() {
        return this.getEntityReferenceDAO().getSession();
    }

    /**
     * Metodo chamado para salvar a entidade. Por default, primeiro veriifca se a entidade {@link #isMappedDirectly(Serializable)}, depois
     * chama o metodo save do seu persistable e por ultimo cria a {@link RemoteEntityReference}
     * 
     * @param entity a entidade
     * @param requestedId {@link Serializable}
     */
    protected void persistEntity(A entity, Serializable requestedId) {
        this.saveWithRequestedId(entity, requestedId);
        entity.getPersister().save();
        this.createRemoteEntityReference(requestedId, entity);
    }

    /**
     * Salva a entidade diretamente pela API do hibernate, se esta {@link #isMappedDirectly(Serializable)}
     * 
     * @param entity a entidade
     * @param requestedId {@link Serializable}
     */
    @SuppressWarnings("deprecation")
    protected void saveWithRequestedId(Persistable entity, Serializable requestedId) {
        if ((requestedId != null) && this.isMappedDirectly(requestedId)) {
            ((org.hibernate.classic.Session) this.getCurrentHibernateSession()).save(entity, requestedId);
        }
    }

    /**
     * @return the restServiceProvider
     */
    public RestServiceProvider getRestServiceProvider() {
        return this.restServiceProvider;
    }

    /**
     * @param restServiceProvider the restServiceProvider to set
     */
    public void setRestServiceProvider(RestServiceProvider restServiceProvider) {
        this.restServiceProvider = restServiceProvider;
    }

    /**
     * @param listener {@link DatabaseSynchronizationListener}
     */
    public void setDatabaseSynchronizationListener(DatabaseSynchronizationListener listener) {
        this.databaseSynchronizationListener = listener;
    }

    /**
     * @return the databaseSynchronizationListener
     */
    public DatabaseSynchronizationListener getDatabaseSynchronizationListener() {
        return this.databaseSynchronizationListener;
    }

}
